package net.ersiner.dmtools.csvmatricizer;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;


/**
 * <p>
 * A Tool for converting compact CSV formatted input containing instance
 * representations as attribute tuples into full matrix form CSV formatted
 * output where each unique attribute is represented by a binary valued column,
 * and attribute occurrences for each instance are represented with a 'T'
 * character (meaning "True").
 * </p>
 * 
 * <p>
 * Resultant format is mostly appropriate for Machine Learning Tools like <a
 * href="http://www.cs.waikato.ac.nz/ml/weka/">Weka.</a>
 * </p>
 * 
 * <p>
 * A sample input, from a shopping basket, can be as follows:
 * 
 * <pre>
 *  Bread,Milk
 *  Bread,Diapers,Beer,Eggs
 *  Milk,Diapers,Beer,Cola
 *  Bread,Milk,Diapers,Beer
 *  Bread,Milk,Diapers,Cola
 * </pre>
 * 
 * Sample output according to the input above would be as follows:
 * 
 * <pre>
 *  Beer,Bread,Cola,Diapers,Eggs,Milk
 *  ,T,,,,T
 *  T,T,,T,T,
 *  T,,T,T,,T
 *  T,T,,T,,T
 *  ,T,T,T,,T
 * </pre>
 * 
 * Note that the attribute list in the output is sorted by name.
 * </p>
 * 
 * @author <a href="http://ersiner.net">Ersin Er</a>
 */
public class CsvMatricizer
{

    private static final String newLine = System.getProperty( "line.separator" );

    private static final CsvMatricizer instance = new CsvMatricizer();

    private CsvMatricizer()
    {

    }

    /**
     * Returns the instance of {@link CsvMatricizer}.
     * 
     * @return The instance of {@link CsvMatricizer}.
     */
    public static CsvMatricizer getInstance()
    {
        return instance;
    }

    /**
     * Converts a compact CSV formatted input into full matrix form CSV
     * formatted output.
     * 
     * @param input
     *            The input source.
     * @param output
     *            The output target.
     * @throws IOException
     */
    public MatricizationResult matricize( Readable input, Appendable output ) throws IOException
    {

        List<String> attrs = new ArrayList<String>();
        List<List<String>> data = new ArrayList<List<String>>();

        // Read instances into a data structure.
        Scanner fs = new Scanner( input );
        while ( fs.hasNextLine() )
        {
            List<String> lineData = new ArrayList<String>();
            Scanner ls = new Scanner( fs.nextLine() );
            ls.useDelimiter( "," );
            while ( ls.hasNext() )
            {
                String attr = ls.next().trim();
                lineData.add( attr );
                attrs.add( attr );
            }
            lineData = new ArrayList<String>( new HashSet<String>( lineData ) );
            Collections.sort( lineData );
            data.add( lineData );
        }
        fs.close();

        // Eliminate duplicates in the attribute list and sort it.
        attrs = new ArrayList<String>( new HashSet<String>( attrs ) );
        Collections.sort( attrs );

        // Write the header line as the sorted list of attribute names.
        int c = 0;
        for ( String attr : attrs )
        {
            output.append( attr );
            c++;
            if ( c < attrs.size() )
            {
                output.append( ',' );
            }
        }
        if ( attrs.size() > 0 )
        {
            output.append( newLine );
        }

        // Write all attribute values per instance.
        for ( List<String> inst : data )
        {
            c = 0;
            int n = -1;
            int last = 0;
            for ( String attr : inst )
            {
                n = attrs.indexOf( attr );
                for ( int i = last; i < n; i++ )
                {
                    c++;
                    output.append( ',' );
                }
                last = n + 1;
                output.append( 'T' );
                if ( c < attrs.size() - 1 )
                {
                    c++;
                    output.append( ',' );
                }
            }

            // Write any remaining commas for missing values.
            for ( int j = n + 1; j < attrs.size(); j++ )
            {
                c++;
                if ( c < attrs.size() )
                {
                    output.append( ',' );
                }
            }
            output.append( newLine );
        }

        return new MatricizationResult( data.size(), attrs.size() );
    }

    /**
     * Entry point for using {@link CsvMatricizer} as an executable tool. Reads
     * from Standard Input and writes to Standard Output.
     * 
     * @param args
     *            N/A
     */
    public static void main( String[] args )
    {

        Reader input = new BufferedReader( new InputStreamReader( System.in ) );
        OutputStreamWriter output = new OutputStreamWriter( System.out );

        try
        {
            CsvMatricizer.getInstance().matricize( input, output );
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }
        finally
        {
            try
            {
                input.close();
                output.close();
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }
    }
}
